'************************************************************************************
' Copyright  2003-2005 Kathleen Dollard
'
' This software is provided 'as-is', without any express or implied warranty. In no 
' event will the author be held liable for any damages arising from the use of this
' software.
' 
' You may use this software for any purpose, including commercial applications, 
' and you may redistribute this software as part of application delivery: however, 
' this copyright notice may not be removed and you may not otherwise redistribute 
' this software directly other than providing a link to wwww.gendotnet.com. 
'
' You may alter this software to better server your needs (I'd like to hear about your 
' changes in order to make the software better). If you alter the softwre, you must  
' clearly mark it as altered and must not be misrepresented as the original software.
' nor wholly your original work.
'*************************************************************************************
'  Summary: Tools for working with hash codes
'  Refactor: When a standard for hashing becomes available - swtich to it!

Option Strict On
Option Explicit On 

Imports system

Namespace KADGen.Utility
   Public Enum FileChanged
      Unknown
      FileDoesntExist
      NotChanged
      Changed
      WhitespaceOnly
      NotSemantic
   End Enum

   Public Class HashTools
      Private Shared mHashMarker As String
      Private Shared mHeaderMarker As String
      Private Const vbCrLf As String = Microsoft.VisualBasic.vbCrLf

      Public Shared Function IsFileChanged( _
                     ByVal fileName As String, _
                     ByVal language As String, _
                     ByVal commentStart As String, _
                     ByVal commentEnd As String, _
                     ByVal headerMarker As String, _
                     ByVal hashFullMarker As String, _
                     ByVal hashSemanticMarker As String, _
                     ByVal hashNoWSMarker As String) _
                     As FileChanged
         Dim inStream As IO.FileStream
         Dim reader As IO.StreamReader = Nothing
         Dim writer As IO.StreamWriter = Nothing
         Try
            If Not IO.File.Exists(fileName) Then
               Return FileChanged.FileDoesntExist
               'ElseIf (IO.File.GetAttributes(fileName) And 1) <> 0 Then
               '   ' Assume a checked out file has been changed
               '   Return FileChanged.Changed
            Else
               inStream = New IO.FileStream(fileName, IO.FileMode.Open, IO.FileAccess.Read)
               reader = New IO.StreamReader(inStream)
               writer = New IO.StreamWriter(New IO.MemoryStream)
               Return IsTextChanged(reader.ReadToEnd, language, commentStart, commentEnd, headerMarker, hashFullMarker, hashSemanticMarker, hashNoWSMarker)
            End If
         Finally
            Try
               If reader IsNot Nothing Then
                  reader.Close()
               End If
               If writer IsNot Nothing Then
                  writer.Close()
               End If
            Catch
            End Try
         End Try
      End Function

      Public Shared Function IsTextChanged( _
                     ByVal s As String, _
                     ByVal language As String, _
                     ByVal commentStart As String, _
                     ByVal commentEnd As String, _
                     ByVal headerMarker As String, _
                     ByVal hashFullMarker As String, _
                     ByVal hashSemanticMarker As String, _
                     ByVal hashNoWSMarker As String) _
                     As FileChanged
         Dim oldFullHashCode As String
         Dim newFullHashCode As String
         Dim oldNoWSHashCode As String
         Dim newNoWSHashCode As String
         Dim oldSemanticHashCode As String
         Dim newSemanticHashCode As String
         Dim text As String
         Dim fullHeaderMarker As String = commentStart & headerMarker & commentEnd

         oldFullHashCode = GetHash(s, commentStart, commentEnd, hashFullMarker)
         If String.IsNullOrEmpty(oldFullHashCode) Then
            Return FileChanged.Unknown
         Else
            text = StripHeader(s, fullHeaderMarker)
            newFullHashCode = CreateHash(text, language)
            If oldFullHashCode = newFullHashCode Then
               Return FileChanged.NotChanged
            End If

            oldSemanticHashCode = GetHash(s, commentStart, commentEnd, hashSemanticMarker)
            If Not String.IsNullOrEmpty(oldSemanticHashCode) Then
               newSemanticHashCode = CreateHash( _
                  GenDotNet.Condenser.Condense.SemanticCondense(text, language), _
                  language)
               If oldSemanticHashCode = newSemanticHashCode Then
                  Return FileChanged.NotSemantic
               End If
            End If

            oldNoWSHashCode = GetHash(s, commentStart, commentEnd, hashNoWSMarker)
            If Not String.IsNullOrEmpty(oldNoWSHashCode) Then
               newNoWSHashCode = CreateHash(RemoveWhitespace(text), language)
               If oldNoWSHashCode.Length > 0 AndAlso newNoWSHashCode = oldNoWSHashCode Then
                  Return FileChanged.WhitespaceOnly
               Else
                  Return FileChanged.Changed
               End If
            End If
         End If
         Return FileChanged.Unknown
      End Function

      Public Shared Sub ApplyHash( _
                     ByVal fileName As String, _
                     ByVal language As String, _
                     ByVal commentText As String, _
                     ByVal commentStart As String, _
                     ByVal commentEnd As String, _
                     ByVal headerMarker As String, _
                     ByVal hashFullMarker As String, _
                     ByVal hashSemanticMarker As String, _
                     ByVal hashNoWSMarker As String)
         Dim inStream As New IO.FileStream(fileName, IO.FileMode.Open)
         Dim outStream As IO.Stream = ApplyHash(inStream, language, commentText, _
                           commentStart, commentEnd, headerMarker, hashFullMarker, _
                           hashSemanticMarker, hashNoWSMarker)
         inStream.Close()
         inStream = New IO.FileStream(fileName, IO.FileMode.Truncate)
         Dim writer As New IO.StreamWriter(inStream)
         writer.Write(outStream)
      End Sub

      Public Shared Function ApplyHash( _
                     ByVal inStream As IO.Stream, _
                     ByVal language As String, _
                     ByVal commentText As String, _
                     ByVal commentStart As String, _
                     ByVal commentEnd As String, _
                     ByVal headerMarker As String, _
                     ByVal hashFullMarker As String, _
                     ByVal hashSemanticMarker As String, _
                     ByVal hashNoWSMarker As String) _
                     As IO.Stream
         Dim reader As New IO.StreamReader(inStream)
         Dim writer As New IO.StreamWriter(New IO.MemoryStream)
         Dim fullHeaderMarker As String = commentStart & headerMarker & commentEnd

         inStream.Seek(0, IO.SeekOrigin.Begin)
         writer.Write(ApplyHash(reader.ReadToEnd, language, commentText, commentStart, _
                     commentEnd, True, headerMarker, hashFullMarker, hashSemanticMarker, hashNoWSMarker))
         writer.Flush()
         writer.BaseStream.Seek(0, IO.SeekOrigin.Begin)
         Return writer.BaseStream
      End Function

      Public Shared Function ApplyHash( _
                     ByVal s As String, _
                     ByVal language As String, _
                     ByVal commentText As String, _
                     ByVal commentStart As String, _
                     ByVal commentEnd As String, _
                     ByVal isString As Boolean, _
                     ByVal headerMarker As String, _
                     ByVal hashFullMarker As String, _
                     ByVal hashSemanticMarker As String, _
                     ByVal hashNoWSMarker As String) _
                     As String
         Dim sb As New Text.StringBuilder
         Dim hashFullString As String
         Dim hashNoWsString As String
         Dim hashSemanticString As String = String.Empty
         Dim fullHeaderMarker As String = commentStart & headerMarker & commentEnd
         Dim sPos As Int32
         Dim sEnd As Int32
         Dim sOpening As String = String.Empty

         s = StripHeader(s, fullHeaderMarker)
         hashFullString = CreateHash(s, language)
         hashNoWsString = CreateHash(RemoveWhitespace(s), language)
         If language.ToLower() = "vb" Or language.ToLower() = "csharp" Then
            hashSemanticString = CreateHash(GenDotNet.Condenser.Condense.SemanticCondense(s, language), language)
         End If
         If language.ToLower = "xml" Then
            sPos = s.IndexOf("<?xml")
            If sPos >= 0 Then
               sEnd = s.IndexOf("?>")
               If sEnd >= 0 Then
                  sOpening = s.Substring(0, sEnd + 2)
                  s = s.Substring(sEnd + 2)
               End If
            End If
         End If

         sb.Append(sOpening)
         sb.Append(fullHeaderMarker & vbCrLf)
         sb.Append(commentStart & commentText & commentEnd & vbCrLf)
         sb.Append(commentStart & hashFullMarker & hashFullString & hashFullMarker & commentEnd & vbCrLf)
         sb.Append(commentStart & hashNoWSMarker & hashNoWsString & hashNoWSMarker & commentEnd & vbCrLf)
         sb.Append(commentStart & hashSemanticMarker & hashSemanticString & hashSemanticMarker & commentEnd & vbCrLf)
         sb.Append(fullHeaderMarker & vbCrLf)
         sb.Append(s)
         Return sb.ToString
      End Function

      Private Shared Function StripHeader( _
               ByVal s As String, _
               ByVal searchFor As String) _
               As String
         Dim iStart As Int32 = s.IndexOf(searchFor)
         Dim iEnd As Int32
         If iStart >= 0 Then
            iEnd = s.Substring(iStart + searchFor.Length).IndexOf(searchFor) + iStart + searchFor.Length
            If iEnd >= 0 Then
               iEnd = iEnd + searchFor.Length + 2
               If iEnd <= s.Length() Then
                  Return s.Substring(0, iStart) & s.Substring(iEnd)
               Else
                  Return ""
               End If
            Else
               Return s
            End If
         Else
            Return s
         End If
      End Function

      Private Shared Function GetHash( _
                     ByVal s As String, _
                     ByVal commentStart As String, _
                     ByVal commentEnd As String, _
                     ByVal hashMarker As String) _
                     As String
         Dim searchForStart As String = commentStart & hashMarker
         Dim searchForEnd As String = hashMarker & commentEnd
         Dim iStart As Int32 = s.IndexOf(searchForStart)
         Dim iEnd As Int32
         Dim iLen As Int32
         If iStart >= 0 Then
            iStart += searchForStart.Length
            iEnd = s.Substring(iStart).IndexOf(searchForEnd) + iStart - 1
            If iEnd >= 0 Then
               ' Messy way to clean off the crlf
               iLen = iEnd - iStart + 1
               Return s.Substring(iStart, iLen)
            Else
               Return ""
            End If
         Else
            Return ""
         End If

      End Function

      Private Shared Function CreateHash( _
               ByVal s As String, _
               ByVal language As String) _
               As String
         Dim md5 As New System.Security.Cryptography.MD5CryptoServiceProvider
         'Dim writer As New IO.StreamWriter(New IO.MemoryStream)
         Dim hash() As Byte
         Dim input() As Byte
         Dim sb As New Text.StringBuilder
         s = s.Trim
         If language.ToLower = "xml" Then
            s = NormalizeXMLSpace(s)
         End If
         ReDim input(s.Length - 1)
         For i As Int32 = 0 To input.GetUpperBound(0)
            input(i) = CByte(Microsoft.VisualBasic.Asc(s.Chars(i)))
         Next
         '   writer.Write(s)
         '   writer.BaseStream.Seek(0, IO.SeekOrigin.Begin)
         hash = md5.ComputeHash(input)
         For i As Int32 = 0 To hash.GetUpperBound(0)
            sb.Append(hash(i).ToString("x2"))
         Next
         'Dim BitConcat As Int64
         'For i As Int32 = 0 To 1
         '   BitConcat = 0
         '   For j As Int32 = 0 To 7
         '      If (j = 0) Then
         '         BitConcat = BitConcat Or (Convert.ToInt64(hash(j + 8 * i)))
         '      Else
         '         BitConcat = BitConcat Or (Convert.ToInt64(hash(j + 8 * i)) * Convert.ToInt64(2 ^ (j * 8 - 1)))
         '      End If
         '   Next
         '   sb.Append(Convert.ToString(BitConcat))
         'Next
         Return sb.ToString
      End Function

      Private Shared Function NormalizeXMLSpace(ByVal s As String) As String
         Dim reader As New Xml.XmlTextReader(s, Xml.XmlNodeType.Document, Nothing)
         reader.WhitespaceHandling = Xml.WhitespaceHandling.Significant
         Return reader.ReadOuterXml
      End Function

      Private Shared Function RemoveWhitespace(ByVal s As String) As String
         ' I'd like to replace all tabs and spaces with a single space
         ' Then replace all multiple occurrences of crlf (nl) with a single
         ' remove all start of line whitespace
         ' Perhaps remove VB.NET wrapping and line continuation (_ and :)
         ' Remove all spaces adjacent to parenthesis, byval, etc
         Return Text.RegularExpressions.Regex.Replace(s, "[\s]", "")
      End Function

   End Class
End Namespace